﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TFDP.Common;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Data.Schema.Package.Project;
using TFDP.Common.T4;
using Microsoft.Data.Schema.Sql.SchemaModel;
using Microsoft.Data.Schema.SchemaModel;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;

namespace TFDP.SimpleProcessor
{
    [Export(typeof(IGenerator))]
    public class Generator : IGenerator
    {
        private IProcessor processor;
        private IDatabaseProjectNode project;
        private Regex extendedPropertyMatcher;
        private DatabaseTemplateProcessor templateProcessor;

        protected IDatabaseProjectNode Project { get { return project; } }
        protected DatabaseTemplateProcessor TemplateProcessor { get { return templateProcessor; } }

        #region IGenerator Members

        public virtual Type SupportedType
        {
            // Return IModelElement as the supported type
            // as it is the base type of almost anything
            // in the managed DBPROJ environment
            get { return typeof(IModelElement); }
        }

        public void Initialize(IProcessor processor, IDatabaseProjectNode project)
        {
            if (processor == null)
                throw new ArgumentNullException("processor");
            if (project == null)
                throw new ArgumentNullException("project");

            this.processor = processor;
            this.project = project;

            InitializeTemplateProcessor(project);

            extendedPropertyMatcher = new Regex(@"\{ep:(.*?)\}", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);

            InitializeCore();
        }

        protected virtual void InitializeCore()
        {
        }

        private void InitializeTemplateProcessor(IDatabaseProjectNode project)
        {
            if (project == null)
                throw new ArgumentNullException("project");

            templateProcessor = new DatabaseTemplateProcessor();
            templateProcessor.ProjectDirectory = project.ProjectFolder;
            templateProcessor.TemplateDirectory = project.FindTemplateDirectory();
            templateProcessor.ProjectId = project.Caption;
            templateProcessor.CurrentConfiguration = project.ConfigurationProvider.ProjectManager.CurrentConfigName;
            templateProcessor.TemplateRecompile += new EventHandler<TemplateRecompileEventArgs>(templateProcessor_TemplateRecompile);
        }

        public IEnumerable<GeneratedArtifact> Generate(IModelElement element)
        {
            if (IsDisposed)
                throw new ObjectDisposedException(this.GetType().Name);
            if (element == null)
                throw new ArgumentNullException("element");

            List<GeneratedArtifact> retvals = new List<GeneratedArtifact>();

            IEnumerable<TemplateMapping> mappings = FindMappingsForElement(element);
            var ordered = mappings.OrderBy(tm => tm.Order);

            foreach (var mapping in ordered)
            {
                retvals.AddRange(GenerateArtifactsForMapping(element, mapping));
            }

            return retvals;
        }

        protected virtual IEnumerable<GeneratedArtifact> GenerateArtifactsForMapping(IModelElement element, TemplateMapping mapping)
        {
            IEnumerable<GeneratedArtifact> artifacts = GenerateArtifact(element, mapping);

            foreach (GeneratedArtifact artifact in artifacts)
                ApplyPropertiesToArtifact(artifact, mapping.Properties, element.GetName());

            return artifacts;
        }

        private void ApplyPropertiesToArtifact(GeneratedArtifact artifact, IEnumerable<MappingProperty> properties, string elementName)
        {
            foreach (MappingProperty mp in properties)
                artifact.Properties.Add(mp.Key, mp.Value);

            if (artifact.Properties.ContainsKey("GenerateViaCodeModel"))
                artifact.Properties.Add("ElementName", elementName);
        }

        #endregion

        private IEnumerable<GeneratedArtifact> GenerateArtifact(IModelElement element, TemplateMapping mapping)
        {
            if (element == null)
                throw new ArgumentNullException("element");
            if (mapping == null)
                throw new ArgumentNullException("mapping");

            Type targetType = Type.GetType(mapping.Type);

            String elementName = Resolve(mapping.ObjectNameTemplate, element);

            Logger.LogVerbose("Generating artifact " + elementName);

            // Check if the procedure exists but only if
            // the target project is the current project.
            IScriptSourcedModelElement matchedElement = null;
            if (!String.IsNullOrEmpty(elementName) && (mapping.Project == project.Caption || String.IsNullOrEmpty(mapping.Project)))
                matchedElement = project.FindEntity<IScriptSourcedModelElement>(elementName);

            // Obtain the properties of the element
            IDatabaseNode node = element.Node(this.project);

            IEnumerable<GeneratedArtifact> artifacts;

            // Execute the template
            string buff = ProcessTemplate(targetType, mapping.Template, element, node, out artifacts);

            // Return the GenerationItem
            List<GeneratedArtifact> retval = new List<GeneratedArtifact>(artifacts);

            if (!String.IsNullOrEmpty(buff))
            {
                // Determine the path
                string path = (matchedElement == null) ? ComposeArtifactPath(element, mapping) : matchedElement.PrimarySource.SourceName;

                // Write output to temporary file
                string tempFile = Path.GetTempFileName();
                File.WriteAllText(tempFile, buff);

                retval.Add(new GeneratedArtifact(tempFile, path, mapping.Project, matchedElement));
            }

            retval
                .Where(a => a.ModelElement == null)
                .ToList()
                .ForEach(a => 
                {
                    a.ProjectName = mapping.Project;
                    if(a.Properties.ContainsKey("ElementName"))
                        a.ModelElement = project.FindEntity<IScriptSourcedModelElement>(a.Properties["ElementName"]);
                }
            );

            return retval;
        }

        protected virtual string ProcessTemplate(Type targetType, string templateFile, IModelElement element, IDatabaseNode node, out IEnumerable<GeneratedArtifact> artifacts)
        {
            if (targetType == null)
                throw new ArgumentNullException("targetType");
            if (String.IsNullOrEmpty(templateFile))
                throw new ArgumentNullException("templateFile");
            if (element == null)
                throw new ArgumentNullException("element");
            if (node == null)
                throw new ArgumentNullException("node");

            // We have to call the method via reflection because of the generic
            // paramter for GenerateWithTemplate.
            // Might change that in the future but probably not.
            MethodInfo method = typeof(DatabaseTemplateProcessor).GetMethod("GenerateWithTemplate");
            MethodInfo generic = method.MakeGenericMethod(targetType);

            object[] parameters = new object[] { templateFile, element, node, null };

            string retval = generic.Invoke(templateProcessor, parameters) as string;

            artifacts = (IEnumerable<GeneratedArtifact>)parameters[3];

            return retval;
        }

        protected string ComposeArtifactPath(IModelElement element, TemplateMapping mapping)
        {
            if (element == null)
                throw new ArgumentNullException("element");
            if (mapping == null)
                throw new ArgumentNullException("mapping");

            if (mapping.Project == project.Caption)
                return String.Format("{0}{1}", project.DirectoryName, Resolve(mapping.FileNameTemplate, element));
            else
                return String.Format(Resolve(mapping.FileNameTemplate, element));
        }

        IModelElement current;

        protected string Resolve(string format, IModelElement element)
        {
            if (String.IsNullOrEmpty(format))
                throw new ArgumentNullException("format");
            if (element == null)
                throw new ArgumentNullException("element");

            string retval = format.Replace("{schema}", element.GetSchemaName());
            retval = retval.Replace("{element}", element.GetName());
            retval = retval.Replace("{elementfullname}", element.GetFullName());

            current = element;

            retval = extendedPropertyMatcher.Replace(
                retval,
                new MatchEvaluator(Matcher)
            );

            current = null;

            return retval;
        }

        private TemplateMappings mappings;

        private IEnumerable<TemplateMapping> FindMappingsForElement(IModelElement element)
        {
            if (element == null)
                throw new ArgumentNullException("element");

            if (mappings == null)
            {
                if (processor.Settings.ContainsKey("Mappings"))
                    this.mappings = processor.Settings.Get<TemplateMappings>("Mappings");
                else
                    this.mappings = new TemplateMappings();
            }

            var matches = mappings
                .Where(tm =>
                    Type.GetType(tm.Type) != null &&
                    Type.GetType(tm.Type).IsInstanceOfType(element) &&
                    tm.Enabled)
                .Select(tm => tm);

            return matches.ToList();
        }

        private string Matcher(Match m)
        {
            return current.GetExtendedProperty<string>(m.Groups[1].Value) ?? String.Empty;
        }

        private void templateProcessor_TemplateRecompile(object sender, TemplateRecompileEventArgs e)
        {
            RaiseTemplateRecompile(e);
        }

        private void RaiseTemplateRecompile(TemplateRecompileEventArgs e)
        {
            if (TemplateRecompile != null)
            {
                TemplateRecompile(this, e);
            }
        }

        public event EventHandler<TemplateRecompileEventArgs> TemplateRecompile;

        private bool isDisposed;
        private object disposeLock = new object();

        public bool IsDisposed
        {
            get
            {
                lock (disposeLock)
                {
                    return isDisposed;
                }
            }
        }

        public void Dispose()
        {
            if (!IsDisposed)
            {
                lock (disposeLock)
                {
                    isDisposed = true;
                }

                Dispose(true);
            }
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (templateProcessor != null)
                {
                    templateProcessor.Dispose();
                    templateProcessor = null;
                }

                // Clean-up references
                extendedPropertyMatcher = null;
                processor = null;
                project = null;
                mappings = null;
            }

            GC.SuppressFinalize(this);
        }
    }
}
